Programming and management of experiments in oTree
Module 2: Individual experiments
Types of experiments
Individual and group experiments
- In terms of programming, we can identify two main types of experiments:
- Individual experiments: each participant plays the game alone
- The payoffs are computed based on the choices of the participant
- Surveys are a special case of individual experiments without payoffs associated to actions
- The payoffs are computed based on the choices of the participant
- Group experiments: each participant plays the game with other participants
- The payoffs are computed based on the choices of the participant and the choices of the other participants
- Individual experiments: each participant plays the game alone
Individual experiments
- Each player chooses independently
- Payoffs are defined by parameters and by her own choice
- Each choice is statistically independent from the others
Group experiments
- Each player chooses independently
- Payoffs are defined by parameters and by her own choice and the choices of the other players
- Choices are statistically dependent
Individual experiments
Multiple Price List
- We investigate individuals’ risk attitudes
- Multiple Price List (MPL) questionnaire (Holt and Laury 2002)
- Trade-off between lotteries
- Multiple Price List (MPL) questionnaire (Holt and Laury 2002)
- As an example,
- Do you prefer a lottery that gives you 2 euros with probability 0.5 and 1.6 euros with probability 0.5, or a lottery that gives you 3.85 euros with probability 0.5 and 0.1 euros with probability 0.5?
Risk attitudes
- A Multiple Price List format
- For each pair of options the participants they should choose A or B
- Option A is safer than corresponding Option B
- The attractiveness of Option B in terms of relative expected payoffs increases when scrolling down to the bottom of the table
- A risk-neutral decision maker should switch from A to B at \(5^{th}\) choice
- Choosing A at \(10^{th}\) choice is dominated for all preference types
- One row is randomly selected and outcomes paid
Screens
|
|
|
|
|
Code
_init_.py (models)
- Import module
random- Needed for payoff computation
- Define session constants
- Outcomes of lotteries in MPL
- Group and Subsession classes are empty
- Typical of one-shot individual decision making
- Grouping is not defined
import random
from otree.api import *
# Author and description
author = 'MP'
doc = """
MPL risk elicitation à la Holt&Laury
"""
# Constants for the experiment (payoffs, app name, etc.)
class C(BaseConstants):
NAME_IN_URL = 'MPL' # App name for URL
PLAYERS_PER_GROUP = None # No grouping
NUM_ROUNDS = 1 # Single round
# Payoff values for lotteries A and B
A_h = 2.00 # Lottery A high payoff
A_l = 1.60 # Lottery A low payoff
B_h = 3.85 # Lottery B high payoff
B_l = 0.10 # Lottery B low payoff
# Group class: used for grouping players (not used in this app, but required by oTree)
class Group(BaseGroup):
pass
# Subsession class: represents a round of the experiment (not used here, but required by oTree)
class Subsession(BaseSubsession):
pass_init_.py (models) (ii)
- In Player class we define the “templates” for data collection
- MPL table
- One variable for each row
- MPL table
class Player(BasePlayer):
# 10 main choices for the MPL table (A or B)
HL_1 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
HL_2 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
HL_3 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
HL_4 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
HL_5 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
HL_6 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
HL_7 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
HL_8 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
HL_9 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
HL_10 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
# Demo choice for instructions
HL = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal, blank=True)
# Questionnaire fields (demographics and feedback)
sex = models.StringField(widget=widgets.RadioSelectHorizontal(), choices=['Male', 'Female', 'Other'])
age = models.IntegerField(choices=range(18, 60, 1))
comment = models.TextField(label="Your comment here:")
like = models.IntegerField(choices=[1, 2, 3, 4, 5], widget=widgets.RadioSelectHorizontal)
# Variables for payoff calculation
row = models.IntegerField() # Randomly selected row for payment
drawn = models.IntegerField() # Random draw for outcome
choice = models.CharField() # Player's choice in selected row__init.py__ (models) (iii)
- After Player class we compute the payoffs
- Typical of individual decision making
# Compute payoff for a player based on random row and outcome
def set_payoff_HL(player: Player):
# Randomly select a row (1-10) for payment
player.row = random.randint(1, 10)
# Randomly select a draw (1-10) to determine outcome
player.drawn = random.randint(1, 10)
# Get the player's choice (A or B) for the selected row
choices = [player.HL_1, player.HL_2, player.HL_3, player.HL_4, player.HL_5,
player.HL_6, player.HL_7, player.HL_8, player.HL_9, player.HL_10]
player.choice = choices[player.row - 1]
# Assign payoff based on choice and draw: if drawn <= row, use high payoff; else, low payoff
if player.drawn <= player.row:
player.payoff = float(C.A_h) if player.choice == "A" else float(C.B_h)
else:
player.payoff = float(C.A_l) if player.choice == "A" else float(C.B_l)
_init.py_ (pages)
- In the instructions we have a simulation of choice protocol
- HL (see models)
- In the main choice page we need to import a form for each row of the MPL table
- We also need the outcomes, retrieved from constants
- Important to “declare” the variables to display with
vars_for_template()
- Important to “declare” the variables to display with
# Instruction page with demo MPL
class Instructions(Page):
form_model = 'player'
form_fields = ['HL'] # Demo choice for instructions
# Main MPL choice page (10 rows)
class PageHL(Page):
form_model = 'player'
form_fields = [
'HL_1', 'HL_2', 'HL_3', 'HL_4', 'HL_5',
'HL_6', 'HL_7', 'HL_8', 'HL_9', 'HL_10',
] # All 10 choices
@staticmethod
def vars_for_template(player: Player):
# Pass payoff values to template for display
return {'A_h': C.A_h, 'A_l': C.A_l, 'B_h': C.B_h, 'B_l': C.B_l}
"""
[...]
"""_init.py_ (pages) (ii)
- Before moving to the next page, we compute payoffs
- See method set_payoff_HL() from models.py
- This way we compute payoffs only once and not when browser is refreshed
- See method set_payoff_HL() from models.py
class PageHL(Page):
"""
[...]
"""
# before moving to next page, compute payoffs (avoids that with refreshing payoffs are recomputed again)
@staticmethod
def before_next_page(player: Player, timeout_happened):
# built-in method
set_payoff_HL(player) # see in models in Player class_init.py_ (pages) (iii)
- Display outcomes
- declare them with vars_for_template()
- retrieve values from participant.vars and “store” them in a dictionary
- declare them with vars_for_template()
# Outcome page: shows the randomly selected row, draw, and payoff
class OutcomeHL(Page):
@staticmethod
def vars_for_template(player: Player):
# Prepare outcome details for the results template:
# - row: which row was randomly selected for payment
# - value: the random draw that determines which outcome is paid
# - choice: player's choice (A or B) in the selected row
# - p_A_1, p_A_2, p_B_1, p_B_2: used for displaying lottery probabilities
return {
'row': player.row, # Randomly chosen row
'value': player.drawn, # Randomly chosen value
'choice': player.choice, # Player's choice
'p_A_1': player.row,
'p_A_2': 10 - player.row,
'p_B_1': player.row,
'p_B_2': 10 - player.row
}_init.py_ (pages) (iv)
- Manage the sequence of pages
# the coreography of pages
page_sequence = [
Instructions,
PageHL,
OutcomeHL
]Templates
Instructions.html
- Style elements
- size of radio buttons
{{ block styles }}
<style>
/* Style radio buttons for better visibility */
input[type=radio] {
transform: scale(1.1);
margin: 12px -10px 0px -30px;
}
</style>
{{ endblock }}Instructions.html (ii)
- Main body and demo of MPL
- In a bs container
{{ block content }}
<!-- Main instructions heading -->
<h1>Instructions</h1>
<!-- Instructional content container -->
<div class="container border" style="font-size16pt" >
<h2> Part 1 </h2>
<!-- Brief description of the first part -->
<p>In the first part of the experiment you are going to choose between couples of lotteries. </p>
<p>The following is an example of the decision setting you are facing</p>
<!-- Example table for one lottery choice -->
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col" colspan="2" style="text-align:center">A</th>
<th scope="col" ></th>
<th scope="col" colspan="2" style="text-align:center">B</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">1</th>
<td > p/10 of €A<sub>1</sub> </td>
<td> (1-p)/10 of €A<sub>2</sub></td>
<!-- Radio button for choice between A and B -->
<td style="font-weight: bold">{{ form.HL }}</td>
<td>p/10 of €B<sub>1</sub></td>
<td >(1-p)/10 of €B<sub>2</sub> </td>
</tr>
</tbody>
</table>
<!-- Explanation of the lottery structure -->
<p>You must choose between lottery A and lottery B, with lottery A delivering €A<sub>1</sub> with probability p/10 and €A<sub>2</sub> with probability (1-p)/10.</p>
<p>Similarly, lottery B delivers €B<sub>1</sub> with probability p/10 and €B<sub>2</sub> with probability (1-p)/10.</p> <p>You will face 10 choices between A and B, with p changing across choices.</p>
<p>All your earnings are virtual, no cash is going to be paid to you. However, choose as if the monetary stakes were real.</p>
</div>
Instructions.html (iii)
- The button to leave the page
- Put it to the right with a container
"""
[...]
"""
<!-- Continue button row -->
<div class="container" style="font-size:18pt">
<div class="row" style="padding-left:135px;">
<div class="col-md-10">
<!-- Empty column for spacing -->
</div>
<div class="col-md-2">
<!-- Continue button to proceed to next page -->
<button name="btn_submit" value="True" class="btn btn-outline-primary btn-large">
<span style="font-size:14pt">Continue</span>
</button>
</div>
</div>
</div>
{{ endblock }}PageHL.html
- Choices are collected in a table
<!-- Table displaying the 10 lottery choices -->
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col" colspan="2" style="text-align:center">A</th>
<th scope="col" ></th>
<th scope="col" colspan="2" style="text-align:center">B</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<!-- Each row represents one lottery choice; radio button in the middle for A/B selection -->
<!-- Row 1 -->
<tr>
<th scope="row">1</th>
<td > 1/10 of €{{A_h}} </td>
<td> 9/10 of €{{A_l}}</td>
<td style="font-weight: bold">{{ form.HL_1 }}</td>
<td>1/10 of €{{B_h}}</td>
<td >9/10 of €{{B_l}} </td>
</tr>
<!-- Row 2 -->
<tr>
<th scope="row">2</th>
<td>2/10 of €{{A_h}} </td>
<td> 8/10 of €{{A_l}}</td>
<td style="font-weight: bold">{{ form.HL_2 }}</td>
<td>2/10 of €{{B_h}}</td>
<td>8/10 of €{{B_l}} </td>
</tr>
<!-- Row 3 -->
<tr>
<th scope="row">3</th>
<td>3/10 of €{{A_h}} </td>
<td> 7/10 of €{{A_l}}</td>
<td style="font-weight: bold">{{ form.HL_3 }}</td>
<td>3/10 of €{{B_h}}</td>
<td>7/10 of €{{B_l}} </td>
</tr>
<!-- Row 4 -->
<tr>
<th scope="row">4</th>
<td>4/10 of €{{A_h}} </td>
<td> 6/10 of €{{A_l}}</td>
<td style="font-weight: bold">{{ form.HL_4 }}</td>
<td>4/10 of €{{B_h}}</td>
<td>6/10 of €{{B_l}} </td>
</tr>
<!-- Row 5 -->
<tr>
<th scope="row">5</th>
<td>5/10 of €{{A_h}} </td>
<td> 5/10 of €{{A_l}}</td>
<td style="font-weight: bold">{{ form.HL_5 }}</td>
<td>5/10 of €{{B_h}}</td>
<td>5/10 of €{{B_l}} </td>
</tr>
<!-- Row 6 -->
<tr>
<th scope="row">6</th>
<td>6/10 of €{{A_h}} </td>
<td> 4/10 of €{{A_l}}</td>
<td style="font-weight: bold">{{ form.HL_6 }}</td>
<td>6/10 of €{{B_h}}</td>
<td>4/10 of €{{B_l}} </td>
</tr>
<!-- Row 7 -->
<tr>
<th scope="row">7</th>
<td>7/10 of €{{A_h}} </td>
<td> 3/10 of €{{A_l}}</td>
<td style="font-weight: bold">{{ form.HL_7 }}</td>
<td>7/10 of €{{B_h}}</td>
<td>3/10 of €{{B_l}} </td>
</tr>
<!-- Row 8 -->
<tr>
<th scope="row">8</th>
<td>8/10 of €{{A_h}} </td>
<td> 2/10 of €{{A_l}}</td>
<td style="font-weight: bold">{{ form.HL_8 }}</td>
<td>8/10 of €{{B_h}}</td>
<td>2/10 of €{{B_l}} </td>
</tr>
<!-- Row 9 -->
<tr>
<th scope="row">9</th>
<td>9/10 of €{{A_h}} </td>
<td> 1/10 of €{{A_l}}</td>
<td style="font-weight: bold">{{ form.HL_9 }}</td>
<td>9/10 of €{{B_h}}</td>
<td>1/10 of €{{B_l}} </td>
</tr>
<!-- Row 10 -->
<tr>
<th scope="row">10</th>
<td>10/10 of €{{A_h}} </td>
<td> 0/10 of €{{A_l}}</td>
<td style="font-weight: bold">{{ form.HL_10 }}</td>
<td>10/10 of €{{B_h}}</td>
<td>0/10 of €{{B_l}} </td>
</tr>
</tbody>
</table>OutcomeHL.html
<!-- Results page heading -->
<h1> Results </h1>
<!-- Container for the outcome summary and payoff details -->
<div class="container border" style="font-size:14pt">
<!-- Inform participant which row was selected for payment -->
<p> <b>Row #{{player.row}}</b> was randomly selected for payment.</p>
<!-- Table summarizing the selected row's lottery structure -->
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col" colspan="2" style="text-align:center">A</th>
<th scope="col" ></th>
<th scope="col" colspan="2" style="text-align:center">B</th>
<th scope="col"></th>
</tr>
</thead>
<tr>
<th scope="row">{{row}}</th>
<td>{{p_A_1}}/10 of €{{C.A_h}} </td>
<td> {{p_A_2}}/10 of €{{C.A_l}}</td>
<td></td>
<td>{{p_B_1}}/10 of €{{C.B_h}}</td>
<td>{{p_B_2}}/10 of €{{C.B_l}} </td>
</tr>
</table>
<!-- Show which lottery the participant chose -->
<p>You chose Lottery <b>{{choice}}</b></p>
<!-- Show the random draw and the resulting payoff -->
<p> The randomly drawn value that defines the outcome of the lottery is {{value}}.</p>
<p> Thus, you earn <b>{{player.payoff}}</b></p>
<!-- Show participant code for reference -->
<p> Your participant code is {{player.participant.code}}</p>
</div>
<!-- Continue button row -->
<div class="container">
<div class="row">
</div>
<div class="row" style="padding-left:130px;">
<div class="col-md-10">
<!-- Empty column for spacing -->
</div>
<div class="col-md-2">
<!-- Button to proceed to the next page -->
<button class="otree-btn-next btn btn-primary">
<span style="font-size:18pt">Continue</span>
</button>
</div>
</div>
</div>
{{ endblock }}
- The randomly chosen row is displayed
{row} - They learn about their payoff
An efficient and stylish way to build the table
- We can use the following code in
__init.py__to build the table (seePageHL_2)
@staticmethod
def vars_for_template(player: Player):
# Build a table of lottery options for display in the template.
# Each row in the table represents one decision (out of 10),
# and contains the details for lottery A, lottery B, and the field name for the choice input.
lottery_table = []
for i in range(1, 11):
# Define the structure for lottery A in this row
lottery_a = {
'prob_high': i, # Probability (out of 10) of high payoff
'prob_low': 10 - i, # Probability (out of 10) of low payoff
'payoff_high': C.A_h, # High payoff amount for A
'payoff_low': C.A_l, # Low payoff amount for A
'description': f"{i}/10 chance of €{C.A_h}, {10-i}/10 chance of €{C.A_l}"
}
# Define the structure for lottery B in this row
lottery_b = {
'prob_high': i, # Probability (out of 10) of high payoff
'prob_low': 10 - i, # Probability (out of 10) of low payoff
'payoff_high': C.B_h, # High payoff amount for B
'payoff_low': C.B_l, # Low payoff amount for B
'description': f"{i}/10 chance of €{C.B_h}, {10-i}/10 chance of €{C.B_l}"
}
# Append the row to the table, including the field name for the choice input (e.g., HL_1, HL_2, ...)
lottery_table.append({
'row_number': i,
'lottery_a': lottery_a,
'lottery_b': lottery_b,
'choice_field': f'HL_{i}' # Field name for this row's choice
})
# Return the table and constants for use in the template
return {
'lottery_table': lottery_table, # List of all rows for the table
'A_h': C.A_h,
'A_l': C.A_l,
'B_h': C.B_h,
'B_l': C.B_l
}An efficient and stylish way to build the table (ii)
- Then in the template we can use the following code to display the table
<!-- Main MPL table -->
<table class="table">
<!-- Table header -->
<thead>
<tr>
<th>Row</th>
<th>Lottery A</th>
<th colspan="2">Your Choice</th> <!-- Spans 2 columns for A and B choices -->
<th>Lottery B</th>
</tr>
</thead>
<!-- Table body with lottery rows -->
<tbody>
<!-- Loop through each row in the lottery table (generated from Python) -->
{% for row in lottery_table %}
<tr>
<!-- Row number (1-10) -->
<td>{{ row.row_number }}</td>
<!-- Lottery A description (probabilities and payoffs) -->
<td>{{ row.lottery_a.description }}</td>
<!-- Choice A radio button - centered -->
<td style="text-align: center;">
<!-- Radio button for choosing lottery A -->
<input type="radio" name="{{ row.choice_field }}" value="A"> A
</td>
<!-- Choice B radio button - centered -->
<td style="text-align: center;">
<!-- Radio button for choosing lottery B -->
<input type="radio" name="{{ row.choice_field }}" value="B"> B
</td>
<!-- Lottery B description (probabilities and payoffs) -->
<td>{{ row.lottery_b.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>An efficient and stylish way to build the table (iii)
- JS to handle the choice selection
{{ block scripts }}
<!-- JavaScript to handle radio button interactions and highlighting -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Wait for the DOM to be fully loaded before executing
// Get all radio buttons in the form
const radioButtons = document.querySelectorAll('input[type="radio"]');
console.log('Found radio buttons:', radioButtons.length); // Debug: log number of radio buttons found
// Add event listener to each radio button for the 'change' event
radioButtons.forEach(function(radio) {
radio.addEventListener('change', function() {
console.log('Radio changed:', this.name, this.value); // Debug: log which radio was changed
// Get the table row that contains this radio button
const row = this.closest('tr');
const cells = row.querySelectorAll('td');
console.log('Found cells in row:', cells.length); // Debug: log number of cells in row
// Remove any existing choice classes from all cells in this row
// This ensures only one lottery is highlighted per row
cells.forEach(cell => {
cell.classList.remove('cell-choice-a', 'cell-choice-b');
});
// Add the appropriate highlighting class based on the selected value
if (this.value === 'A') {
// Highlight lottery A column (second cell - index 1)
if (cells[1]) {
cells[1].classList.add('cell-choice-a');
console.log('Added cell-choice-a to cell 1'); // Debug: confirm A highlighting
}
} else if (this.value === 'B') {
// Highlight lottery B column (last cell - index 4)
if (cells[4]) {
cells[4].classList.add('cell-choice-b');
console.log('Added cell-choice-b to cell 4'); // Debug: confirm B highlighting
}
}
});
// Also add click event as backup in case 'change' doesn't fire
radio.addEventListener('click', function() {
// Manually trigger the change event
this.dispatchEvent(new Event('change'));
});
});
// Check for any pre-selected values on page load
// This handles cases where the form might have existing values
radioButtons.forEach(function(radio) {
if (radio.checked) {
console.log('Pre-selected radio found:', radio.name, radio.value); // Debug: log pre-selected values
const row = radio.closest('tr');
const cells = row.querySelectorAll('td');
// Remove any existing choice classes from all cells
cells.forEach(cell => {
cell.classList.remove('cell-choice-a', 'cell-choice-b');
});
// Apply highlighting based on pre-selected value
if (radio.value === 'A') {
// Highlight lottery A column (second cell - index 1)
if (cells[1]) {
cells[1].classList.add('cell-choice-a');
}
} else if (radio.value === 'B') {
// Highlight lottery B column (last cell - index 4)
if (cells[4]) {
cells[4].classList.add('cell-choice-b');
}
}
}
});
});
</script>
{{ endblock }} - css to highlight the selected choice
{{ block styles }}
<style>
/* Radio button styling - make them larger and more visible */
input[type=radio] {
transform: scale(1.5);
/* Make radio buttons 1.5x larger */
/* margin: 12px -10px 0px -30px; */
/* Optional margin adjustment (commented out) */
}
/* Style for selected radio buttons - blue accent color */
input[type=radio]:checked {
accent-color: #007bff;
/* Bootstrap primary blue */
background-color: #007bff;
}
/* Fallback styling for older browsers that don't support accent-color */
input[type=radio]:checked::before {
background-color: #007bff;
}
/* Cell background colors for visual feedback when choices are made */
td.cell-choice-a {
background-color: #dddddd !important;
/* Light grey for lottery A selection */
transition: background-color 0.3s ease;
/* Smooth color transition */
}
td.cell-choice-b {
background-color: #dddddd !important;
/* Same grey for lottery B selection */
transition: background-color 0.3s ease;
/* Smooth color transition */
}
</style>
{{ endblock }}Appendix
Assignment 1
- Reasonably easy
- Add a third option in each row: “Indifferent”
- The choice should look like
- The choice should look like
- Add a third option in each row: “Indifferent”
- Difficult
- Allow people to “switch” only once from A to B
- As an example, if a participant chooses B in row 4, all rows <4 “automatically” become A and all rows >4 become B
- Allow people to “switch” only once from A to B
oTree code
- The oTree app of this lecture:
MPL
References
References
Holt, Charles A, and Susan K Laury. 2002. “Risk Aversion and Incentive Effects.” American Economic Review 92 (5): 1644–55.